ウホっ?!KongでEC2上にAmazon API Gatewayもどきの環境を構築してLambda & Cognitoユーザープールと連携させてみた?
CX事業本部@大阪の岩田です。サーバーレスアーキテクチャの鉄板であるAmazon API Gateway × Lambdaという構成のメリットとして完全従量課金であるという点が挙げられますが、頻繁に利用される環境では従量課金が逆にデメリットとなり、自前でEC2上にアプリを構築する方が安く上がるというケースも有りえます。このブログではAmazon API Gatewayの代替としてEC2上にKongの環境を構築し、Lambda & Cognitoユーザープールと連携させてみます。
環境
今回構築した環境です
- OS: Amazon Linux2
- AMI ID: amzn2-ami-hvm-2.0.20200617.0-x86_64-gp2 (ami-06ad9296e6cf1e3cf)
- インスタンスタイプ: m5.large
- PostgreSQL :11.5
- Kong: 2.1.0
Kongとは?
OSSで開発されている、いわゆるAPI Gatewayです。普段このブログで「API Gateway」というと「Amazon API Gateway」というサービスを指すことがほとんどですが、ここでの「API Gateway」はもう少し広い意味合いで、複数のマイクロサービスの前段でクライアントに対して単一のエントリポイントを提供し
- 認証/認可
- ロギング
- レートリミット
- キャッシュ
- 負荷分散
等々各サービスに必要な機能を横断的に提供するコンポーネントを指します。(広義の)API Gatewayについてはマイクロサービスアーキテクチャパターンとして以下のリンクで紹介されているので、よければ参考にして下さい。
Pattern: API Gateway / Backends for Frontends
Kongに関する詳細な説明は割愛します。少し古いですが、Kongについてはこちらのブログでも紹介されているので、参考にして下さい。
やってみる
AWS上にKongの環境を構築するには
- Dockerイメージ
- AMI
- CloudFormationのテンプレート
等が利用可能ですが、今回はせっかくなのでEC2上に自力で構築してみようと思います。
まずはAmazon Linux2のEC2を起動して進めていきます。後ほどLambdaと連携させる際に便利なので、LambdaのInvoke権限を持ったロールを事前に作成し、EC2にアタッチしておいて下さい。
PostgreSQLのインストール
Kongが利用するバックエンドのDBはPostgreSQLもしくはCassandraが選択可能です。本来はアクセス増に伴ってスケールアウト可能なCassandraを利用するのが望ましいと思いますが、今回はお試しなのでPostgreSQLを利用します。ということで、まずはPostgreSQLの環境を構築します。
まずはインストール
$ sudo amazon-linux-extras install -y postgresql11 $ sudo yum install -y postgresql-server
DBクラスタを初期化します
$ sudo su - postgres $ initdb $ exit
デーモンを起動
$ sudo systemctl start postgresql
Kong用にデータベースとユーザーを作成します。psqlから以下のSQLを実行します。
$ psql -U postgres psql (11.5) Type "help" for help. postgres=# CREATE USER kong; CREATE DATABASE kong OWNER kong; CREATE ROLE CREATE DATABASE postgres=# exit
一旦ここまででOKです
Kongのインストール&初期設定
続いてKongのインストールと初期設定です。今回はKong公式のRPMファイルを利用します。
依存関係の解決で利用するため、epelを有効化します。
$ sudo amazon-linux-extras install -y epel
RPMファイルをDLしてインストールします
$ curl -L https://bintray.com/kong/kong-rpm/download_file?file_path=amazonlinux/amazonlinux/kong-2.1.0.aws.amd64.rpm -O $ sudo yum install -y kong-2.1.0.aws.amd64.rpm
設定ファイルの雛形をコピーして設定ファイルを作成し、Kongを初期化します
$ sudo cp /etc/kong/kong.conf.default /etc/kong/kong.conf $ sudo /usr/local/bin/kong migrations bootstrap Bootstrapping database... migrating core on database 'kong'... core migrated up to: 000_base (executed) core migrated up to: 003_100_to_110 (executed) core migrated up to: 004_110_to_120 (executed) core migrated up to: 005_120_to_130 (executed) core migrated up to: 006_130_to_140 (executed) core migrated up to: 007_140_to_150 (executed) core migrated up to: 008_150_to_200 (executed) core migrated up to: 009_200_to_210 (executed) migrating rate-limiting on database 'kong'... rate-limiting migrated up to: 000_base_rate_limiting (executed) rate-limiting migrated up to: 003_10_to_112 (executed) rate-limiting migrated up to: 004_200_to_210 (executed) migrating hmac-auth on database 'kong'... hmac-auth migrated up to: 000_base_hmac_auth (executed) hmac-auth migrated up to: 002_130_to_140 (executed) hmac-auth migrated up to: 003_200_to_210 (executed) migrating oauth2 on database 'kong'... oauth2 migrated up to: 000_base_oauth2 (executed) oauth2 migrated up to: 003_130_to_140 (executed) oauth2 migrated up to: 004_200_to_210 (executed) migrating ip-restriction on database 'kong'... ip-restriction migrated up to: 001_200_to_210 (executed) migrating jwt on database 'kong'... jwt migrated up to: 000_base_jwt (executed) jwt migrated up to: 002_130_to_140 (executed) jwt migrated up to: 003_200_to_210 (executed) migrating basic-auth on database 'kong'... basic-auth migrated up to: 000_base_basic_auth (executed) basic-auth migrated up to: 002_130_to_140 (executed) basic-auth migrated up to: 003_200_to_210 (executed) migrating key-auth on database 'kong'... key-auth migrated up to: 000_base_key_auth (executed) key-auth migrated up to: 002_130_to_140 (executed) key-auth migrated up to: 003_200_to_210 (executed) migrating session on database 'kong'... session migrated up to: 000_base_session (executed) migrating acl on database 'kong'... acl migrated up to: 000_base_acl (executed) acl migrated up to: 002_130_to_140 (executed) acl migrated up to: 003_200_to_210 (executed) migrating response-ratelimiting on database 'kong'... response-ratelimiting migrated up to: 000_base_response_rate_limiting (executed) migrating bot-detection on database 'kong'... bot-detection migrated up to: 001_200_to_210 (executed) migrating acme on database 'kong'... acme migrated up to: 000_base_acme (executed) 34 migrations processed 34 executed Database is up-to-date
Kongを起動します
$ sudo systemctl start kong
動作確認してみましょう
$ curl http://localhost:8001/ {"plugins":{"enabled_in_cluster":[],"available_on_server":{"grpc-web":true,"correlation-id":true,"pre-function":true,"cors":true,"rate-limiting":true,"loggly":true,"hmac-auth":true,"zipkin":true,"request-size-limiting":true,"azure-functions":true,"request-transformer":true,"oauth2":true,"response-transformer":true, ...略
レスポンスが返却されればOKです。
KongとLambdaを連携する
続いてKongとLambdaを連携させてみましょう。Kongのエンドポイントに対して、/lambda1というパスでアクセスされた場合にEC2からLambdaを起動してレスポンスを返却するように設定します。
まず事前に適当なLambdaを作成しておきます。イベントデータをそのままJSONで返却するだけのLambdaを作成しました。
import json def handler(event, context): return { 'statusCode': 200, 'body': json.dumps(event) }
このLambdaをKongのバックエンドとして統合していきます。
まずはサービスを作成します。ここではlambda1
という名前でサービスを作成します。
curl -i -X POST http://localhost:8001/services \ --data 'name=lambda1' \ --data 'url=http://localhost:8000' HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:33:27 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 353 X-Kong-Admin-Latency: 157 {"host":"localhost","id":"898427b9-8356-4143-a492-1f575857ea5b","protocol":"http","read_timeout":60000,"tls_verify_depth":null,"port":8000,"updated_at":1596069207,"ca_certificates":null,"created_at":1596069207,"connect_timeout":60000,"write_timeout":60000,"name":"lambda1","retries":5,"path":null,"tls_verify":null,"tags":null,"client_certificate":null}
続いて、作成したサービスlambda1
に/lambda1
というルートを追加します。
$ curl -i -X POST http://localhost:8001/services/lambda1/routes \ --data 'paths[1]=/lambda1' HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:34:07 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 427 X-Kong-Admin-Latency: 5 {"id":"4fe908ac-8fef-412b-b328-971cf583e7b7","path_handling":"v0","paths":["\/lambda1"],"destinations":null,"headers":null,"protocols":["http","https"],"created_at":1596069247,"snis":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"name":null,"strip_path":true,"preserve_host":false,"regex_priority":0,"updated_at":1596069247,"sources":null,"methods":null,"https_redirect_status_code":426,"hosts":null,"tags":null}
idが発行されるので控えておきましょう。この例だと4fe908ac-8fef-412b-b328-971cf583e7b7
がidです。
続いてlambda1
というサービスからLambdaをInvokeするためにKongのプラグインを有効化します。
$ curl -i -X POST http://localhost:8001/services/lambda1/plugins \ --data 'name=aws-lambda' \ --data 'config.aws_region=ap-northeast-1' \ --data 'config.function_name=<実行したいLambda Function名>' HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:35:05 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 742 X-Kong-Admin-Latency: 7 {"created_at":1596069305,"id":"0b01909b-c987-4b13-82e4-2fb14f983688","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"aws-lambda","consumer":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"route":null,"config":{"host":null,"timeout":60000,"skip_large_bodies":true,"keepalive":60000,"port":443,"forward_request_method":false,"awsgateway_compatible":false,"log_type":"Tail","aws_region":"ap-northeast-1","aws_secret":null,"is_proxy_integration":false,"proxy_scheme":null,"unhandled_status":null,"invocation_type":"RequestResponse","forward_request_body":false,"forward_request_uri":false,"aws_key":null,"forward_request_headers":false,"qualifier":null,"proxy_url":null,"function_name":"<登録したLambda Function名>"}}
これでKong経由でLambdaを実行する準備ができました。試しに先程登録したルートにアクセスしてみましょう。先程まではKongの管理用APIを実行するために8001ポートにアクセスしていましたが、今度は一般ユーザー向けのエンドポイントにアクセスするため8000ポートへアクセスします。
$ curl http://localhost:8000/lambda1/ {"statusCode": 200, "body": "{}"}
Lambda Functionのレスポンスが返却されてきました。成功です。ただbodyは空のJSONになっており、Lambda Functionにイベントデータが渡せていないようです。このあたりはAmazon API GatewayとLambdaをプロキシ統合した場合とは大きく振る舞いが変わってきそうです。
試しに今度はクエリストリングを付与してリクエストしてみましょう
$curl http://localhost:8000/lambda1/?foo=bar {"statusCode": 200, "body": "{\"foo\": \"bar\"}"}
今度はレスポンスボディにクエリストリングで指定した内容が返却されてきました。GETリクエストの場合はイベントデータとしてクエリストリングがLambdaに渡されているようです。
KongとCognitoユーザープールを連携する
Amazon API Gatewayのユースケースとして定番のLambdaとの連携ですが、ここまでの作業でKongでも代替可能になりました。続いてAmazon API Gatewayの定番ユースケースであるCognitoユーザープールとの連携に挑戦します。Cognitoユーザープールから発行された妥当なトークンを持つユーザーだけAPIにアクセス可能になるのがゴールです。Cognitoユーザープールの作成手順などは特に説明しませんので、事前に適当なユーザープールを作成しておいて下さい。
まず先程作成したlambda1
というサービスでjwtのプラグインを有効化します。Kongにはjwtのプラグインが提供されており、このプラグインを利用することでjwtのトークンを検証することが可能になります。
$ curl -i -X POST http://localhost:8001/services/lambda1/plugins --data "name=jwt" HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:42:42 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 462 X-Kong-Admin-Latency: 5 {"created_at":1596069762,"id":"c2eb3b21-39a9-4a45-beb0-42b24701b950","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"jwt","consumer":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"route":null,"config":{"secret_is_base64":false,"run_on_preflight":true,"uri_param_names":["jwt"],"key_claim_name":"iss","header_names":["authorization"],"maximum_expiration":0,"anonymous":null,"claims_to_verify":null,"cookie_names":[]}}
続いて/lambda1
というルートでjwtのプラグインを利用するように設定を追加します。先程控えておいた/lambda1というルートのIDを指定してプラグイン有効化のリクエストを発行します。
$ curl -i -XPOST http://localhost:8001/routes/<lambda1のルートID>/plugins/ --data "name=jwt" HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:43:52 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 462 X-Kong-Admin-Latency: 5 {"created_at":1596069832,"id":"bc01c780-251b-4432-b144-f6ae483c0c93","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"jwt","consumer":null,"service":null,"route":{"id":"4fe908ac-8fef-412b-b328-971cf583e7b7"},"config":{"secret_is_base64":false,"run_on_preflight":true,"uri_param_names":["jwt"],"key_claim_name":"iss","header_names":["authorization"],"maximum_expiration":0,"anonymous":null,"claims_to_verify":null,"cookie_names":[]}}
コンシューマを作成します。
$ curl -i -XPOST http://localhost:8001/consumers/ --data "custom_id=1" HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:45:24 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 113 X-Kong-Admin-Latency: 4 {"custom_id":"1","created_at":1596069924,"id":"88860da7-8e57-41ed-96a7-33fc729ee787","tags":null,"username":null}
custom_idの指定は適当な値でOKです。リクエストに成功したら発行されたコンシューマのIDを控えます。この例だと88860da7-8e57-41ed-96a7-33fc729ee787
がIDです。
続いてコンシューマの設定を行います。JWTのトークンを検証するためにCognitoユーザープールの公開鍵を登録する必要があるので、Cognitoのエンドポイントから公開鍵を取得します。
https://cognito-idp.ap-northeast-1.amazonaws.com/<ユーザープールID>/.well-known/jwks.json
というURLにアクセスします。
こんな感じで公開鍵が2つ公開されていると思います。
{ "keys": [ { "alg": "RS256", "e": "AQAB", "kid": "IlW7VDIMQC0m3F2LAPll3aiF8HDXn+F4U0hceYLSkBg=", "kty": "RSA", "n": "vrS_gIkdgbN-8tuaQkGBBXnqimovHNP7FrXHHDfmwtlkvbCWC25DPlgrRIyVTJNqzGCywHbwdxqOfZSfdT2-6fXe045WQDlojHH4Fdnylqg_PXB1vYz_6jIXdmdOR61rQLuJrZDKzdCs2YirWUeUIkFyDGdnt2O8pOeV9QPgBScotuQG5564rHHPD_GVbzRcbqKq9s8mp4K3od4Pqfc8npeIyam79tdgv4Xbevc1Y45FmtMsQBhOrtrMXR2BMwoE6NVClgMLFiAss1A9neOFli074Dy1ajtkLoGwPROtK3h25o-4nh7ql2tq5zgGLbj8_SoLommlfPoFReS28xVMbw", "use": "sig" }, { "alg": "RS256", "e": "AQAB", "kid": "gJsi1T1yiYW3bovOEhU39skvFSFVgwXVjnFjpuYONOU=", "kty": "RSA", "n": "zr7tPCgEKNmUUIjq8oca5uHXL_IRcd9m3b3i_PGdGLEbdXczyXW17QJdG19MVDNeS-0vAkcaXHtLDoHhzaVK3U5NNmImr8NEfmb7Lmvu3e0TR3BDZ1Vql4aFr9Un93oLojJLGVWwKFadfrcr0D3YhkJ0j0RR8MNW_MK4xViqrTgImcI4IErZTnTwVkmN8Z7iZKmqroYqEKUGq9qYYMetp3Oq4oJs_VHdn2cTmMSEk6gQD61qVKZnzn3A9wzWFMtpE-u5HdXCRn66EOZdnwisle2SImKV0j-cV-oBM3KxtPGsTJNKCOtTIuCg9PmL7g1Pkc1xI1Z_Burp3aGRIimjcw", "use": "sig" } ] }
この2つの公開鍵を登録...したいところなのですが、Kongでは1つのissuer対して1つの公開鍵しか登録できないようなので、片方だけ登録します。順番としてはメチャクチャなのですが、先にCognitoユーザープールからIDトークンを発行しておき、発行されたIDトークンをjwt.ioで確認し、発行されたIDトークンとkidが一致する方のjwkオブジェクトをコピーします。
続いてコピーした公開鍵をpem形式に変換します。以下のサイトを利用するとオンラインでjwk -> pemに変換可能です。jwkのオブジェクトを丸々入力欄に貼り付けて変換しましょう。
https://8gwifi.org/jwkconvertfunctions.jsp
変換前
{ "alg": "RS256", "e": "AQAB", "kid": "IlW7VDIMQC0m3F2LAPll3aiF8HDXn+F4U0hceYLSkBg=", "kty": "RSA", "n": "vrS_gIkdgbN-8tuaQkGBBXnqimovHNP7FrXHHDfmwtlkvbCWC25DPlgrRIyVTJNqzGCywHbwdxqOfZSfdT2-6fXe045WQDlojHH4Fdnylqg_PXB1vYz_6jIXdmdOR61rQLuJrZDKzdCs2YirWUeUIkFyDGdnt2O8pOeV9QPgBScotuQG5564rHHPD_GVbzRcbqKq9s8mp4K3od4Pqfc8npeIyam79tdgv4Xbevc1Y45FmtMsQBhOrtrMXR2BMwoE6NVClgMLFiAss1A9neOFli074Dy1ajtkLoGwPROtK3h25o-4nh7ql2tq5zgGLbj8_SoLommlfPoFReS28xVMbw", "use": "sig" }
変換後
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvrS/gIkdgbN+8tuaQkGB BXnqimovHNP7FrXHHDfmwtlkvbCWC25DPlgrRIyVTJNqzGCywHbwdxqOfZSfdT2+ 6fXe045WQDlojHH4Fdnylqg/PXB1vYz/6jIXdmdOR61rQLuJrZDKzdCs2YirWUeU IkFyDGdnt2O8pOeV9QPgBScotuQG5564rHHPD/GVbzRcbqKq9s8mp4K3od4Pqfc8 npeIyam79tdgv4Xbevc1Y45FmtMsQBhOrtrMXR2BMwoE6NVClgMLFiAss1A9neOF li074Dy1ajtkLoGwPROtK3h25o+4nh7ql2tq5zgGLbj8/SoLommlfPoFReS28xVM bwIDAQAB -----END PUBLIC KEY-----
変換後の公開鍵をEC2上に保存します。ここではjwt.pemという名前で保存したことにします。
公開鍵をコンシューマーに登録します。
$ curl -i -XPOST http://localhost:8001/consumers/<コンシューマのID>/jwt -F "algorithm=RS256" -F "[email protected]" -F "key=https://cognito-idp.ap-northeast-1.amazonaws.com/<CognitoユーザープールのID>" HTTP/1.1 201 Created Date: Thu, 30 Jul 2020 00:50:21 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 773 X-Kong-Admin-Latency: 5 {"created_at":1596070221,"id":"7d82d9f3-a1a7-4db3-99d9-fecbd6127108","tags":null,"secret":"Ek6n07I7MwwuomNN7OuANso8ow3PAyZs","rsa_public_key":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz9LMY57G\/fHGS9PBbAp5\n9EkB3YXHvaYzoBrVolsNRyBL2mBPf\/wHoqhUGwS4YD9rAsfNGqlN3neriwWJNrey\nvmRJxVkkMIYwajlB7cQPZ5SvVLtsu9eDt4HIie9oc6kxfH9YGI48u9Y1NuRqwyYc\nB4xRkrlMeA1IN8yfNfxGBF9gxaFtd4A0sBjG4+T5ngvswzsu87NFIB6QjrsNUrzq\n8cUqy5a6of547GJQcVI5f4JMcnRbL7cC3o7d5GuCezgo+nbzUVv8RJlQ3zKoIlns\nNFYRzp+m1b00keOc6pJ99ZRVXcm0SULU+A1oUf\/3pY8W1LzJGk\/wW0i8H8HlXg20\nRQIDAQAB\n-----END PUBLIC KEY-----\n\n","consumer":{"id":"88860da7-8e57-41ed-96a7-33fc729ee787"},"key":"https:\/\/cognito-idp.ap-northeast-1.amazonaws.com\/ap-northeast-1_fTJgs3leV","algorithm":"RS256"}
[email protected]
の部分で、先程EC2に保存した公開鍵を指定しています。
jwtトークンの有効期限を検証するように設定を更新しておきます
$ curl -i -XPATCH curl http://localhost:8001/plugins/<jwtプラグインのID> --data "config.claims_to_verify=exp" HTTP/1.1 200 OK Date: Thu, 30 Jul 2020 00:52:42 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Access-Control-Allow-Origin: * Server: kong/2.1.0 Content-Length: 465 X-Kong-Admin-Latency: 6 {"created_at":1596069762,"id":"c2eb3b21-39a9-4a45-beb0-42b24701b950","tags":null,"enabled":true,"protocols":["grpc","grpcs","http","https"],"name":"jwt","consumer":null,"service":{"id":"898427b9-8356-4143-a492-1f575857ea5b"},"route":null,"config":{"secret_is_base64":false,"run_on_preflight":true,"uri_param_names":["jwt"],"key_claim_name":"iss","header_names":["authorization"],"maximum_expiration":0,"anonymous":null,"claims_to_verify":["exp"],"cookie_names":[]}}
準備完了です。まず先程と同様に普通にLambda用のエンドポイントにアクセスしてみましょう。Cognitoユーザープールと統合しているので、未認証のリクエストはエラーになるのが期待値です。
$ curl http://localhost:8000/lambda1 {"message":"Unauthorized"}
期待値通りエラーになりました。
続いてAuthorizationヘッダにCognitoユーザープールから発行されたIDトークンをセットして、再度リクエストを試みます。
$ curl http://localhost:8000/lambda1 -H "Authorization:Bearer <Cognitoユーザープールから発行されたIDトークン>" {"statusCode": 200, "body": "{}"}
今度は無事リクエストに成功しました!
最後にデタラメなトークンを使ってリクエストしてみましょう
curl http://localhost:8000/lambda1 -H "Authorization:Bearer hogehoge" {"message":"Bad token; invalid JSON"}
jwtの検証エラーになりました
これでAmazon API GatewayをKongに置き換えた風の構成が完成です。
まとめ
Amazon API Gateway & Cognitoユーザープール & Lambdaというサーバーレスアーキテクチャの鉄板構成からAmazon API GatewayをKongに置き換えてみました。マネージドサービスの従量課金のコストがネックになる場合、かつしっかりとした運用体制が取れる場合はKongのようなOSSを自前運用するという選択肢もアリかもしれません。
もし自前運用を検討される場合は、単に従量課金のコストだけで評価するのではなく、OSやミドルウェアレイヤの保守対応に必要なマンパワーも含めた上で評価して頂くようにお願いします。